class CountUp {
  constructor(target, endVal, options) {
    this.endVal = endVal;
    this.options = options;
    this.options = {
      ...this.defaults,
      ...options
    };
    this.formattingFn = this.options.formattingFn ? this.options.formattingFn : this.formatNumber;
    this.easingFn = this.options.easingFn ? this.options.easingFn : this.easeOutExpo;
    this.startVal = this.validateValue(this.options.startVal);
    this.frameVal = this.startVal;
    this.endVal = this.validateValue(endVal);
    this.options.decimalPlaces = Math.max(this.options.decimalPlaces ?? 0);
    this.resetDuration();
    this.options.separator = String(this.options.separator);
    this.useEasing = this.options.useEasing;
    if (this.options.separator === "") {
      this.options.useGrouping = false;
    }
    this.el = typeof target === "string" ? document.getElementById(target) : target;
    if (this.el) {
      this.printValue(this.startVal);
    } else {
      this.error = "[CountUp] target is null or undefined";
    }
    if (typeof window !== "undefined" && this.options.enableScrollSpy) {
      if (!this.error) {
        window["onScrollFns"] = window["onScrollFns"] || [];
        window["onScrollFns"].push(() => this.handleScroll(this));
        window.onscroll = () => {
          window["onScrollFns"].forEach((fn) => fn());
        };
        this.handleScroll(this);
      } else {
        console.error(this.error, target);
      }
    }
  }
  version = "2.8.0";
  defaults = {
    startVal: 0,
    decimalPlaces: 0,
    duration: 2,
    useEasing: true,
    useGrouping: true,
    useIndianSeparators: false,
    smartEasingThreshold: 999,
    smartEasingAmount: 333,
    separator: ",",
    decimal: ".",
    prefix: "",
    suffix: "",
    enableScrollSpy: false,
    scrollSpyDelay: 200,
    scrollSpyOnce: false
  };
  rAF;
  startTime;
  remaining = 0;
  finalEndVal = null;
  // for smart easing
  useEasing = true;
  countDown = false;
  el;
  formattingFn;
  easingFn;
  error = "";
  startVal = 0;
  duration = 0;
  paused = true;
  frameVal;
  once = false;
  handleScroll(self) {
    if (!self || !window || self.once)
      return;
    const bottomOfScroll = window.innerHeight + window.scrollY;
    const rect = self.el.getBoundingClientRect();
    const topOfEl = rect.top + window.pageYOffset;
    const bottomOfEl = rect.top + rect.height + window.pageYOffset;
    if (bottomOfEl < bottomOfScroll && bottomOfEl > window.scrollY && self.paused) {
      self.paused = false;
      setTimeout(() => self.start(), self.options?.scrollSpyDelay);
      if (self.options?.scrollSpyOnce)
        self.once = true;
    } else if ((window.scrollY > bottomOfEl || topOfEl > bottomOfScroll) && !self.paused) {
      self.reset();
    }
  }
  /**
   * Smart easing works by breaking the animation into 2 parts, the second part being the
   * smartEasingAmount and first part being the total amount minus the smartEasingAmount. It works
   * by disabling easing for the first part and enabling it on the second part. It is used if
   * useEasing is true and the total animation amount exceeds the smartEasingThreshold.
   */
  determineDirectionAndSmartEasing() {
    const end = this.finalEndVal ? this.finalEndVal : this.endVal;
    this.countDown = this.startVal > end;
    const animateAmount = end - this.startVal;
    if (Math.abs(animateAmount) > (this.options.smartEasingThreshold ?? 0) && this.options?.useEasing) {
      this.finalEndVal = end;
      const up = this.countDown ? 1 : -1;
      this.endVal = end + up * (this.options.smartEasingAmount ?? 0);
      this.duration = (this.duration ?? 0) / 2;
    } else {
      this.endVal = end;
      this.finalEndVal = null;
    }
    if (this.finalEndVal !== null) {
      this.useEasing = false;
    } else {
      this.useEasing = this.options.useEasing;
    }
  }
  // start animation
  start(callback) {
    if (this.error) {
      return;
    }
    if (this.options.onStartCallback) {
      this.options.onStartCallback();
    }
    if (callback) {
      this.options.onCompleteCallback = callback;
    }
    if (this.duration > 0) {
      this.determineDirectionAndSmartEasing();
      this.paused = false;
      this.rAF = requestAnimationFrame(this.count);
    } else {
      this.printValue(this.endVal);
    }
  }
  // pause/resume animation
  pauseResume() {
    if (!this.paused) {
      cancelAnimationFrame(this.rAF);
    } else {
      this.startTime = void 0;
      this.duration = this.remaining;
      this.startVal = this.frameVal;
      this.determineDirectionAndSmartEasing();
      this.rAF = requestAnimationFrame(this.count);
    }
    this.paused = !this.paused;
  }
  // reset to startVal so animation can be run again
  reset() {
    cancelAnimationFrame(this.rAF);
    this.paused = true;
    this.resetDuration();
    this.startVal = this.validateValue(this.options.startVal);
    this.frameVal = this.startVal;
    this.printValue(this.startVal);
  }
  // pass a new endVal and start animation
  update(newEndVal) {
    cancelAnimationFrame(this.rAF);
    this.startTime = void 0;
    this.endVal = this.validateValue(newEndVal);
    if (this.endVal === this.frameVal) {
      return;
    }
    this.startVal = this.frameVal;
    if (this.finalEndVal == null) {
      this.resetDuration();
    }
    this.finalEndVal = null;
    this.determineDirectionAndSmartEasing();
    this.rAF = requestAnimationFrame(this.count);
  }
  count = (timestamp) => {
    if (!this.startTime) {
      this.startTime = timestamp;
    }
    const progress = timestamp - this.startTime;
    this.remaining = this.duration - progress;
    if (this.useEasing && this.easingFn) {
      if (this.countDown) {
        this.frameVal = this.startVal - this.easingFn(progress, 0, this.startVal - this.endVal, this.duration);
      } else {
        this.frameVal = this.easingFn(progress, this.startVal, this.endVal - this.startVal, this.duration);
      }
    } else {
      this.frameVal = this.startVal + (this.endVal - this.startVal) * (progress / this.duration);
    }
    const wentPast = this.countDown ? this.frameVal < this.endVal : this.frameVal > this.endVal;
    this.frameVal = wentPast ? this.endVal : this.frameVal;
    this.frameVal = Number(this.frameVal.toFixed(this.options.decimalPlaces));
    this.printValue(this.frameVal);
    if (progress < this.duration) {
      this.rAF = requestAnimationFrame(this.count);
    } else if (this.finalEndVal !== null) {
      this.update(this.finalEndVal);
    } else {
      if (this.options.onCompleteCallback) {
        this.options.onCompleteCallback();
      }
    }
  };
  printValue(val) {
    if (!this.el)
      return;
    const result = this.formattingFn(val);
    if (this.options.plugin?.render) {
      this.options.plugin.render(this.el, result);
      return;
    }
    if (this.el.tagName === "INPUT") {
      const input = this.el;
      input.value = result;
    } else if (this.el.tagName === "text" || this.el.tagName === "tspan") {
      this.el.textContent = result;
    } else {
      this.el.innerHTML = result;
    }
  }
  ensureNumber(n) {
    return typeof n === "number" && !isNaN(n);
  }
  validateValue(value) {
    const newValue = Number(value);
    if (!this.ensureNumber(newValue)) {
      this.error = `[CountUp] invalid start or end value: ${value}`;
      return 0;
    } else {
      return newValue;
    }
  }
  resetDuration() {
    this.startTime = void 0;
    this.duration = Number(this.options.duration) * 1e3;
    this.remaining = this.duration;
  }
  // default format and easing functions
  formatNumber = (num) => {
    const neg = num < 0 ? "-" : "";
    let result, x1, x2, x3;
    result = Math.abs(num).toFixed(this.options.decimalPlaces);
    result += "";
    const x = result.split(".");
    x1 = x[0];
    x2 = x.length > 1 ? this.options.decimal + x[1] : "";
    if (this.options.useGrouping) {
      x3 = "";
      let factor = 3, j = 0;
      for (let i = 0, len = x1.length; i < len; ++i) {
        if (this.options.useIndianSeparators && i === 4) {
          factor = 2;
          j = 1;
        }
        if (i !== 0 && j % factor === 0) {
          x3 = this.options.separator + x3;
        }
        j++;
        x3 = x1[len - i - 1] + x3;
      }
      x1 = x3;
    }
    if (this.options.numerals && this.options.numerals.length) {
      x1 = x1.replace(/[0-9]/g, (w) => this.options.numerals?.[+w] + "");
      x2 = x2.replace(/[0-9]/g, (w) => this.options.numerals?.[+w] + "");
    }
    return neg + this.options.prefix + x1 + x2 + this.options.suffix;
  };
  // t: current time, b: beginning value, c: change in value, d: duration
  easeOutExpo = (t, b, c, d) => c * (-Math.pow(2, -10 * t / d) + 1) * 1024 / 1023 + b;
}

export { CountUp };
